/*
 * Copyright 2017 - 2025 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.mail;

import org.jspecify.annotations.Nullable;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;

import infra.util.ObjectUtils;

/**
 * Exception thrown when a mail sending error is encountered.
 * Can register failed messages with their exceptions.
 *
 * @author Dmitriy Kopylenko
 * @author Juergen Hoeller
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
 * @since 4.0
 */
public class MailSendException extends MailException {

  private final transient Map<Object, Exception> failedMessages;

  private final Exception @Nullable [] messageExceptions;

  /**
   * Constructor for MailSendException.
   *
   * @param msg the detail message
   * @param cause the root cause from the mail API in use
   */
  public MailSendException(@Nullable String msg, @Nullable Throwable cause) {
    super(msg, cause);
    this.failedMessages = new LinkedHashMap<>();
    this.messageExceptions = null;
  }

  /**
   * Constructor for registration of failed messages, with the
   * messages that failed as keys, and the thrown exceptions as values.
   * <p>The messages should be the same that were originally passed
   * to the invoked send method.
   *
   * @param msg the detail message
   * @param cause the root cause from the mail API in use
   * @param failedMessages a Map of failed messages as keys and thrown
   * exceptions as values
   */
  public MailSendException(@Nullable String msg, @Nullable Throwable cause, Map<Object, Exception> failedMessages) {
    super(msg, cause);
    this.failedMessages = new LinkedHashMap<>(failedMessages);
    this.messageExceptions = failedMessages.values().toArray(new Exception[0]);
  }

  /**
   * Constructor for registration of failed messages, with the
   * messages that failed as keys, and the thrown exceptions as values.
   * <p>The messages should be the same that were originally passed
   * to the invoked send method.
   *
   * @param failedMessages a Map of failed messages as keys and thrown
   * exceptions as values
   */
  public MailSendException(Map<Object, Exception> failedMessages) {
    this(null, null, failedMessages);
  }

  /**
   * Return a Map with the failed messages as keys, and the thrown exceptions
   * as values.
   * <p>Note that a general mail server connection failure will not result
   * in failed messages being returned here: A message will only be
   * contained here if actually sending it was attempted but failed.
   * <p>The messages will be the same that were originally passed to the
   * invoked send method, that is, SimpleMailMessages in case of using
   * the generic MailSender interface.
   * <p>In case of sending MimeMessage instances via JavaMailSender,
   * the messages will be of type MimeMessage.
   * <p><b>NOTE:</b> This Map will not be available after serialization.
   * Use {@link #getMessageExceptions()} in such a scenario, which will
   * be available after serialization as well.
   *
   * @return the Map of failed messages as keys and thrown exceptions as values
   * @see SimpleMailMessage
   * @see jakarta.mail.internet.MimeMessage
   */
  public final Map<Object, Exception> getFailedMessages() {
    return this.failedMessages;
  }

  /**
   * Return an array with thrown message exceptions.
   * <p>Note that a general mail server connection failure will not result
   * in failed messages being returned here: A message will only be
   * contained here if actually sending it was attempted but failed.
   *
   * @return the array of thrown message exceptions,
   * or an empty array if no failed messages
   */
  public final Exception[] getMessageExceptions() {
    return (this.messageExceptions != null ? this.messageExceptions : new Exception[0]);
  }

  @Override
  @Nullable
  public String getMessage() {
    if (ObjectUtils.isEmpty(this.messageExceptions)) {
      return super.getMessage();
    }
    else {
      StringBuilder sb = new StringBuilder();
      String baseMessage = super.getMessage();
      if (baseMessage != null) {
        sb.append(baseMessage).append(". ");
      }
      sb.append("Failed messages: ");
      for (int i = 0; i < this.messageExceptions.length; i++) {
        Exception subEx = this.messageExceptions[i];
        sb.append(subEx.toString());
        if (i < this.messageExceptions.length - 1) {
          sb.append("; ");
        }
      }
      return sb.toString();
    }
  }

  @Override
  public String toString() {
    if (ObjectUtils.isEmpty(this.messageExceptions)) {
      return super.toString();
    }
    else {
      StringBuilder sb = new StringBuilder(super.toString());
      sb.append("; message exceptions (").append(this.messageExceptions.length).append(") are:");
      for (int i = 0; i < this.messageExceptions.length; i++) {
        Exception subEx = this.messageExceptions[i];
        sb.append('\n').append("Failed message ").append(i + 1).append(": ");
        sb.append(subEx);
      }
      return sb.toString();
    }
  }

  @Override
  public void printStackTrace(PrintStream ps) {
    if (ObjectUtils.isEmpty(this.messageExceptions)) {
      super.printStackTrace(ps);
    }
    else {
      ps.println(super.toString() + "; message exception details (" +
              this.messageExceptions.length + ") are:");
      for (int i = 0; i < this.messageExceptions.length; i++) {
        Exception subEx = this.messageExceptions[i];
        ps.println("Failed message " + (i + 1) + ":");
        subEx.printStackTrace(ps);
      }
    }
  }

  @Override
  public void printStackTrace(PrintWriter pw) {
    if (ObjectUtils.isEmpty(this.messageExceptions)) {
      super.printStackTrace(pw);
    }
    else {
      pw.println(super.toString() + "; message exception details (" +
              this.messageExceptions.length + ") are:");
      for (int i = 0; i < this.messageExceptions.length; i++) {
        Exception subEx = this.messageExceptions[i];
        pw.println("Failed message " + (i + 1) + ":");
        subEx.printStackTrace(pw);
      }
    }
  }

}
